/*
 * Copyright 2012 Netflix, Inc.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.netflix.eureka;

import com.netflix.appinfo.*;
import com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DeploymentContext;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.converters.JsonXStream;
import com.netflix.discovery.converters.XmlXStream;
import com.netflix.eureka.aws.AwsBinder;
import com.netflix.eureka.aws.AwsBinderDelegate;
import com.netflix.eureka.cluster.PeerEurekaNodes;
import com.netflix.eureka.registry.AwsInstanceRegistry;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;
import com.netflix.eureka.resources.DefaultServerCodecs;
import com.netflix.eureka.resources.ServerCodecs;
import com.netflix.eureka.util.EurekaMonitors;
import com.thoughtworks.xstream.XStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Date;

/**
 * The class that kick starts the eureka server.
 *
 * <p>
 * The eureka server is configured by using the configuration
 * {@link EurekaServerConfig} specified by <em>eureka.server.props</em> in the
 * classpath.  The eureka client component is also initialized by using the
 * configuration {@link EurekaInstanceConfig} specified by
 * <em>eureka.client.props</em>. If the server runs in the AWS cloud, the eureka
 * server binds it to the elastic ip as specified.
 * </p>
 *
 * @author Karthik Ranganathan, Greg Kim, David Liu
 */
public class EurekaBootStrap implements ServletContextListener {
    private static final Logger logger = LoggerFactory.getLogger(EurekaBootStrap.class);

    private static final String TEST = "test";

    private static final String ARCHAIUS_DEPLOYMENT_ENVIRONMENT = "archaius.deployment.environment";

    private static final String EUREKA_ENVIRONMENT = "eureka.environment";

    private static final String CLOUD = "cloud";
    private static final String DEFAULT = "default";

    private static final String ARCHAIUS_DEPLOYMENT_DATACENTER = "archaius.deployment.datacenter";

    private static final String EUREKA_DATACENTER = "eureka.datacenter";

    protected volatile EurekaServerContext serverContext;
    protected volatile AwsBinder awsBinder;

    private EurekaClient eurekaClient;

    /**
     * Construct a default instance of Eureka boostrap
     */
    public EurekaBootStrap() {
        this(null);
    }

    /**
     * Construct an instance of eureka bootstrap with the supplied eureka client
     *
     * @param eurekaClient the eureka client to bootstrap
     */
    public EurekaBootStrap(EurekaClient eurekaClient) {
        this.eurekaClient = eurekaClient;
    }

    /**
     * eureka-server启动初始化的入口方法
     * web.xml中配置的 listener ，web容器启动后会调用 listener 的 contextInitialized 方法
     * <p>
     * Initializes Eureka, including syncing up with other Eureka peers and publishing the registry.
     *
     * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        try {
            // 初始化环境
            initEurekaEnvironment();
            // 初始化 EurekaServerContext
            initEurekaServerContext();

            ServletContext sc = event.getServletContext();
            sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
        } catch (Throwable e) {
            logger.error("Cannot bootstrap eureka server :", e);
            throw new RuntimeException("Cannot bootstrap eureka server :", e);
        }
    }

    /**
     * 初始化 eureka 环境：数据中心和运行环境
     * Users can override to initialize the environment themselves.
     */
    protected void initEurekaEnvironment() throws Exception {
        logger.info("Setting the eureka configuration..");

        // 初始化配置管理器 ConfigurationManager.getConfigInstance()
        // 初始化 ConfigurationManager 实例，配置管理器，管理 eureka 的所有配置，读取配置文件里的配置到内存里，供后续的eureka-server运行来使用
        // 单例模式，经典单例模式 double check + volatile
        // 最终会创建 ConcurrentCompositeConfiguration 实例

        // 初始化数据中心的配置，如果没配置就是默认的数据中心
        String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
        if (dataCenter == null) {
            logger.info("Eureka data center value eureka.datacenter is not set, defaulting to default");
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
        } else {
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
        }
        // 初始化 eureka 运行环境
        String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
        if (environment == null) {
            // 如果没配置的话，默认设置为 测试环境
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
            logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
        }
    }

    /**
     * init hook for server context. Override for custom logic.
     */
    protected void initEurekaServerContext() throws Exception {
        /**
         * 第一步 加载 eureka 配置文件， 加载 eureka-server.properties中的配置
         */
        // EurekaServerConfig 代表了 eureka-server 需要的所有的配置项，通过接口定义了大量的方法，可以从这里获取所有的配置

        // EurekaServerConfig 接口中有一堆 get 方法，包含了 eureka server 需要使用的所有的配置，都可以通过这个接口来获取
        // 想象一下，properties 里面都是一个一个的 key value 配置项，肯定是将这些配置项加载到内存里的 Properties 对象中存放。一般来说，如果我们自己设计读取 properties 的代码，也许就是将配置加载到 properties 对象中就结束了
        // public load(){
        //      Properties prop = new Properties();
        //      prop.load(inputStream);
        // }
        // 然后获取一个配置项，提供一个方法
        // public String get(String key){
        //     return properties.get(key);
        // }
        // 然后在外面要获取一个配置，可能会在一个常量类里面搞一堆的配置常量
        // get(Config.REMOTE_URL);

        // 这种方式也不错，就是大量的基于常量的方式来获取配置属性的
        // 但是这里使用了另外一种思想，针对配置定义接口，接口方法暴露了大量的配置项的获取的方法，直接通过这个接口来获取需要的配置项即可

        // 创建实例的时候会执行 init 方法，在方法中会完成对 eureka-server.properties 配置文件配置项的加载
        /**
         * 加载 eureka-server.properties 的过程：
         * 1. 创建一个 DefaultEurekaServerConfig 对象
         * 2. 创建对象的时候，init方法执行
         * 3. 将 eureka-server.properties 中的配置加载到一个 Properties 对象中，然后将 Properties 对象中的配置放到 ConfigurationManager 中去，此时 ConfigurationManager 中就有了所有的配置了
         * 4. 然后 DefaultEurekaServerConfig 提供的获取配置项的各个方法，都是通过硬编码配置项名称，从 DynamicPropertyFactory 中获取配置项的值，DynamicPropertyFactory 是从 ConfigurationManager 中来的，所以也包含了所有配置项的值
         * 5. 在后去配置项的时候，如果没有配置，就会有默认的值，默认值也是在 DefaultEurekaServerConfig 硬编码的
         *
         *  eureka-server 的配置管理机制的思想。
         *  不是通过一大堆的常量类获取配置项的，体用了一个获取配置的接口，接口里包含大量的方法，每个方法可以获取一个配置，获取配置的方法里对配置的名称进行硬编码
         */
        EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

        // For backward compatibility
        JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
        XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);

        logger.info("Initializing the eureka client...");
        logger.info(eurekaServerConfig.getJsonCodecName());
        ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig);

        /**
         * 第二步 初始化 eureka-server 内部的一个 eureka-client ，用来和其他的 eureka-server 节点注册和通信
         */
        // 初始化 ApplicationInfoManager
        ApplicationInfoManager applicationInfoManager = null;

        if (eurekaClient == null) {
            /**
             * 和 EurekaServerConfig 类似 读取 eureka-client 配置
             * PropertiesInstanceConfig 类 80行 CommonConstants.CONFIG_FILE_NAME 设置配置文件名为 eureka-client
             * 加载 eureka-client.properties 文件中的配置加载到 ConfigurationManager中去
             */
            // 可以认为 EurekaInstanceConfig 是服务实例相关的一些配置， eureka server 同时也是一个 eureka client
            // 因为 eureka 他要向其他的 eureka server 去注册，组成一个 eureka server 的集群
            // eureka server 把自己也当成一个 eureka client
            EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
                    ? new CloudInstanceConfig()
                    : new MyDataCenterInstanceConfig();

            // 服务实例

            // new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get() 返回 InstanceInfo
            // InstanceInfo 当前这个服务实例本身的信息
            // 内部通过构造器模式，构建 InstanceInfo 对象，InstanceInfo.Builder.newBuilder();

            // 基于 EurekaInstanceConfig（配置） 和 InstanceInfo（服务实例） ，构造了一个 ApplicationInfoManager
            // 后面会基于这个 ApplicationInfoManager 对服务实例进行管理
            applicationInfoManager = new ApplicationInfoManager(
                    instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());

            // 包含 EurekaClient 相关的一些配置项，也是去读 eureka-client.properties 里的一些配置
            // 只不过他关注的是跟之前的 EurekaInstanceConfig 是不一样的，EurekaInstanceConfig 代表服务实例的一些配置项，这里关联的是 EurekaClient 的一些配置项
            EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();

            // 基于 ApplicationManager 和 eurekaClientConfig ，一起构建了一个 EurekaClient，但是构建的时候用的是 EurekaClient 的子类 DiscoveryClient
            eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
        } else {
            applicationInfoManager = eurekaClient.getApplicationInfoManager();
        }

        /**
         * 第三步 处理注册相关的功能
         */

        /*
            PeerAware: peers集群，peer就是集群中的一个实例，Aware 感知。
            InstanceRegistry：实例注册，服务实例注册，注册表，这个里面放了所有的注册到这个eureka server上来的服务实例，就是一个服务实例的注册表。
            PeerAwareInstanceRegistry：可以感知eureka server集群的服务实例注册表，eureka client（作为服务实例）过来注册的注册表，而且这个注册表是可以感知到eureka server集群的。
            假如有一个eureka server集群的话，这里包含了其他的eureka server中的服务实例注册表的信息的。
         */
        PeerAwareInstanceRegistry registry;
        // 如果是亚马逊AWS云环境
        if (isAws(applicationInfoManager.getInfo())) {
            registry = new AwsInstanceRegistry(
                    eurekaServerConfig,
                    eurekaClient.getEurekaClientConfig(),
                    serverCodecs,
                    eurekaClient
            );
            awsBinder = new AwsBinderDelegate(eurekaServerConfig, eurekaClient.getEurekaClientConfig(), registry, applicationInfoManager);
            awsBinder.start();
        } else {
            registry = new PeerAwareInstanceRegistryImpl(
                    eurekaServerConfig,
                    eurekaClient.getEurekaClientConfig(),
                    serverCodecs,
                    eurekaClient
            );
        }

        /**
         * 第四步 处理 peer 节点相关的事情
         * 处理 eureka server 集群的初始化
         */
        // PeerEurekaNodes，代表了eureka server集群，peers大概来说多个相同的实例组成的一个集群，peer就是peers集群中的一个实例，PeerEurekaNodes，大概来说，猜测，应该是代表的是eureka server集群
        PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
                registry,
                eurekaServerConfig,
                eurekaClient.getEurekaClientConfig(),
                serverCodecs,
                applicationInfoManager
        );

        /**
         * 第五步 完成 eureka server 上下文 context 的构建以及初始化
         */
        // 将上面构造好的所有的东西，都一起来构造一个EurekaServerContext，代表了当前这个eureka server的一个服务器上下文，包含了服务器需要的所有的东西。
        serverContext = new DefaultEurekaServerContext(
                eurekaServerConfig,
                serverCodecs,
                registry,
                peerEurekaNodes,
                applicationInfoManager
        );

        // 将 EurekaServerContext 放在了一个holder中，以后谁如果要使用直接从这个holder中获取就可以了。
        // 这个也是一个比较常见的用法，就是将初始化好的一些东西，放在一个holder中，然后后面的话呢，整个系统运行期间，谁都可以来获取，在任何地方任何时间，谁都可以获取这个上下文，从里面获取自己需要的一些组件。
        EurekaServerContextHolder.initialize(serverContext);

        // serverContext 的初始化方法，里面调用了 peerEurekaNodes.start() 和 registry.init(peerEurekaNodes);
        serverContext.initialize();
        logger.info("Initialized server context");

        /**
         * 第六步 处理一点注册善后的事情，从相邻的 eureka 节点拷贝注册信息
         */
        // Copy registry from neighboring eureka node
        // 从相邻的一个eureka server节点拷贝注册表的信息，如果自己本地还没注册的服务实例，在自己本地注册
        // 如果拷贝失败，就找下一个
        // 从其他的 eureka server 拉取注册表过来，放到自己本地，作为初始的注册表
        int registryCount = registry.syncUp();

        // registryCount 从相邻的 eureka 节点拉取过来的服务实例的数量，将这个数量做为自己本地的服务数量
        // 打开注册表，接收请求
        registry.openForTraffic(applicationInfoManager, registryCount);

        // Register all monitoring statistics.
        // 第七部 注册所有的监控统计项
        EurekaMonitors.registerAllStats();
    }

    protected PeerEurekaNodes getPeerEurekaNodes(PeerAwareInstanceRegistry registry, EurekaServerConfig eurekaServerConfig, EurekaClientConfig eurekaClientConfig, ServerCodecs serverCodecs, ApplicationInfoManager applicationInfoManager) {
        PeerEurekaNodes peerEurekaNodes = new PeerEurekaNodes(
                registry,
                eurekaServerConfig,
                eurekaClientConfig,
                serverCodecs,
                applicationInfoManager
        );

        return peerEurekaNodes;
    }

    /**
     * Handles Eureka cleanup, including shutting down all monitors and yielding all EIPs.
     *
     * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
     */
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        try {
            logger.info("{} Shutting down Eureka Server..", new Date().toString());
            ServletContext sc = event.getServletContext();
            sc.removeAttribute(EurekaServerContext.class.getName());

            destroyEurekaServerContext();
            destroyEurekaEnvironment();

        } catch (Throwable e) {
            logger.error("Error shutting down eureka", e);
        }
        logger.info("{} Eureka Service is now shutdown...", new Date().toString());
    }

    /**
     * Server context shutdown hook. Override for custom logic
     */
    protected void destroyEurekaServerContext() throws Exception {
        EurekaMonitors.shutdown();
        if (awsBinder != null) {
            awsBinder.shutdown();
        }
        if (serverContext != null) {
            serverContext.shutdown();
        }
    }

    /**
     * Users can override to clean up the environment themselves.
     */
    protected void destroyEurekaEnvironment() throws Exception {

    }

    protected boolean isAws(InstanceInfo selfInstanceInfo) {
        boolean result = DataCenterInfo.Name.Amazon == selfInstanceInfo.getDataCenterInfo().getName();
        logger.info("isAws returned {}", result);
        return result;
    }

    protected boolean isCloud(DeploymentContext deploymentContext) {
        logger.info("Deployment datacenter is {}", deploymentContext.getDeploymentDatacenter());
        return CLOUD.equals(deploymentContext.getDeploymentDatacenter());
    }
}
